// This library implements a cron expr parser and runner
package cron

import (
	"container/heap"
	"crypto/md5"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"time"
)

// Job is an interface for submitted cron jobs.
type Job struct {
	schedule Schedule
	run      func()
	expire   time.Time
	stop     bool
	times    int
	index    string
}

// JobHeap is a sturct for sort cron jobs
type JobHeap []*Job

func (h JobHeap) Len() int            { return len(h) }
func (h JobHeap) Less(i, j int) bool  { return h[i].expire.Before(h[j].expire) }
func (h JobHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h JobHeap) Top() *Job           { return h[0] }
func (h *JobHeap) Pop() interface{}   { r := (*h)[len(*h)-1]; *h = (*h)[0 : len(*h)-1]; return r }
func (h *JobHeap) Push(x interface{}) { *h = append(*h, x.(*Job)) }

func (h *JobHeap) Dump() {
	for _, val := range *h {
		fmt.Println(*val)
	}
}

// Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may be started, stopped, and the entries may
// be inspected while running.
type Cron struct {
	jobs JobHeap
	refs map[string]*Job
	tick *time.Ticker

	add chan *Job
	del chan string

	stop chan struct{}
	dump chan []*Job
}

const (
	interval = 500 * time.Millisecond
)

// New returns a new Cron job runner.
func New() *Cron {
	return &Cron{
		tick: time.NewTicker(interval),
		refs: make(map[string]*Job),
		add:  make(chan *Job),
		del:  make(chan string),
		stop: make(chan struct{}),
		dump: make(chan []*Job),
	}
}

// Add a Job to the Cron to be run on the given schedule.
func (c *Cron) Add(expr string, times int, run func()) (string, error) {
	schedule, err := Parse(expr)
	if err != nil {
		return "", err
	}

	c.add <- &Job{schedule: schedule, times: times, run: run, expire: schedule.Next(time.Now())}
	job := <-c.add
	return job.index, nil
}

func (c *Cron) radd(job *Job) *Job {
	job.index = uuid()
	heap.Push(&c.jobs, job)
	c.refs[job.index] = job
	return job
}

func uuid() string {
	b := make([]byte, 128)
	rand.Read(b)
	h := md5.New()
	h.Write(b)
	return hex.EncodeToString(h.Sum(nil))
}

// Del a job from Cron, won't stop running job
func (c *Cron) Del(index string) error {
	c.del <- index
	ret := <-c.del
	if ret == "" {
		return errors.New("index not found")
	}
	return nil
}

func (c *Cron) rdel(index string) string {
	if job, ok := c.refs[index]; ok {
		job.stop = true
		return index
	}
	return ""
}

// Elapse time then schedule time ticker
func (c *Cron) elapse() {
	now := time.Now()
	size := c.jobs.Len()
	for size > 0 && c.jobs.Top().expire.Before(now) {
		top := heap.Pop(&c.jobs).(*Job)
		if !top.stop && top.times > 0 {
			top.times--
			go top.run()
			top.expire = top.schedule.Next(now)
			heap.Push(&c.jobs, top)
		}
		size = c.jobs.Len()
	}
}

// Start the cron scheduler in its own go-routine.
func (c *Cron) Start() {
	go func() {
		defer c.tick.Stop()
		for {
			select {
			case <-c.tick.C:
				c.elapse()
			case job := <-c.add:
				c.add <- c.radd(job)
			case job := <-c.del:
				c.del <- c.rdel(job)
			case <-c.stop:
				return
			}
		}
	}()
}

// Stop the cron scheduler.
func (c *Cron) Stop() {
	c.stop <- struct{}{}
}
